/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved.
 */

import { cloneDeep, isEqual } from '@baize/wdk';
import { DurationEvent, FileSet, hvigorCore, MetricLogType, TaskInputValue } from '@ohos/hvigor';
import fse from 'fs-extra';

import { findTargetProduct } from '../../common/find-target-product.js';
import { projectOhosConfigManager } from '../../common/global/project-ohos-config-manager.js';
import { ModulePathInfoIml } from '../../common/iml/module-path-info-iml.js';
import { ProjectPathInfoIml } from '../../common/iml/project-path-info-iml.js';
import { CommonConst, HvigorConfigConst } from '../../const/common-const.js';
import { SignTypeEnum } from '../../enum/sign-type-enum.js';
import { ECM } from '../../error/error-code-map.js';
import { ProjectModel } from '../../model/project/project-model.js';
import { ProjectBuildProfile } from '../../options/build/project-build-profile';
import { SdkInfo } from '../../sdk/sdk-info.js';
import { getElementFromArr } from '../../utils/array-util';
import { FileUtil } from '../../utils/file-util.js';
import { InjectUtil } from '../../utils/inject-util.js';
import { KeyStoreHelper } from '../../utils/keystore-utils.js';
import { OhosLogger } from '../../utils/log/ohos-logger.js';
import { ProcessUtils } from '../../utils/process-utils';
import { ValidateUtil } from '../../utils/validate/validate-util.js';
import { ModuleTaskService } from '../service/module-task-service';
import { TaskService } from '../service/task-service';

import { CommonSignCommandBuilder } from './command-builder-impl/common-sign-command-builder.js';
import { SignCommandFactory } from './command-builder-impl/sign-command-factory';
import { SignModel } from './command-builder-impl/sign-model';
import { constructHarSignParams, constructSignParams, sendSignHarRequest, sendSignRequest } from './sign-request.js';
import ProductBuildOpt = ProjectBuildProfile.ProductBuildOpt;
import SigningConfigBuildOpt = ProjectBuildProfile.SigningConfigBuildOpt;
import MaterialBuildOpt = ProjectBuildProfile.MaterialBuildOpt;

/**
 * 执行签名的工具类
 *
 * @since 2022/1/21
 */
export class SignUtil {
  private _log: OhosLogger = OhosLogger.getLogger(SignUtil.name);

  private readonly _taskService: TaskService;
  private readonly _signType: SignTypeEnum;
  private readonly _targetProduct: ProductBuildOpt;
  private readonly _signCommandFactory: SignCommandFactory;
  protected readonly _pathInfo: ModulePathInfoIml | ProjectPathInfoIml;
  private signingConfigCheckLogStr: string | undefined;
  private readonly sdkInfo: SdkInfo;

  constructor(
    taskService: TaskService,
    signType: SignTypeEnum,
    pathInfo: ModulePathInfoIml | ProjectPathInfoIml,
    targetProduct: ProjectBuildProfile.ProductBuildOpt,
    sdkInfo: SdkInfo,
  ) {
    const apiVersion: number = taskService.getProjectModel().getCompileApiMetaByProduct(targetProduct.name).version;
    this._taskService = taskService;
    this._signType = signType;
    this._targetProduct = targetProduct;
    this._signCommandFactory = new SignCommandFactory(apiVersion);
    this._pathInfo = pathInfo;
    this.sdkInfo = sdkInfo;
  }

  get _signingConfig(): ProjectBuildProfile.SigningConfigBuildOpt | undefined {
    return this.getSigningConfig();
  }

  get _signingConfigCheckLogStr(): string | undefined {
    this.signingConfigCheckLogStr = undefined;
    this.getSigningConfig();
    return this.signingConfigCheckLogStr;
  }

  /**
   * 执行签名公共接口
   *
   * @param {SignModel} signModel 签名类型、输入、输出参数
   * @param durationEvent 持续事件
   */
  async sign(signModel: SignModel, durationEvent: DurationEvent) {
    if (signModel.getSignType() === SignTypeEnum.HAR) {
      try {
        await this.executeSignRequest(signModel, durationEvent);
      } catch (e: any) {
        this._log.errorMessageExit(`Remote sign har failed. errorCode: ${e?.code ?? ''}`);
      }
      return;
    }

    // daemon 模式且新 sdk 才支持 java daemon
    if (InjectUtil.isDaemon() && this.sdkInfo.supportHapSignToolApiCall(!this.sdkInfo.isOhos)) {
      try {
        await this.executeSignRequest(signModel, durationEvent);
      } catch (e: any) {
        // 任何情况下只要远程签名报错都回退尝试用本地java -jar重新签一次
        this._log.debug(`java daemon remote sign failed ${e?.code} fallback to local java -jar sign`);
        await this.executeSign(signModel, durationEvent);
      }
    } else {
      await this.executeSign(signModel, durationEvent);
    }
    if (!fse.existsSync(signModel.getOutPutFilePath()) && signModel.getSignType() === SignTypeEnum.SHELL) {
      fse.copyFileSync(signModel.getInputFilePath(), signModel.getOutPutFilePath());
    }
  }

  getSignCommand(signModel: SignModel) {
    if (!this._signingConfig) {
      // 签名配置不存在，则不执行签名
      return undefined;
    }
    const commandBuilder: CommonSignCommandBuilder = this._signCommandFactory.createCommandBuilder(
      this._taskService.getProjectModel(),
      this._signingConfig!,
      this.sdkInfo,
      signModel,
    );
    return commandBuilder.getSignCommand()!;
  }

  getBundleNameFromHap(projectModel: ProjectModel) {
    return ValidateUtil.getBundleNameFromHap(this._targetProduct, projectModel);
  }

  getVerifySignCommands(pathInfo: ModulePathInfoIml) {
    return ValidateUtil.getVerifySignCommands(this.sdkInfo, this._signingConfig, pathInfo as ModulePathInfoIml);
  }

  /**
   * 签名配置文件与hap包BundleName校验
   *
   * @param {SignModel | undefined} signModel
   * @param {ModulePathInfoIml | ProjectPathInfoIml} pathInfo
   * @param subDurationEvent 子持续事件
   * @param useJavaDaemon 是否使用 java daemon
   * @private
   */
  async validateBundleName(
    signModel: SignModel | undefined,
    pathInfo: ModulePathInfoIml | ProjectPathInfoIml,
    subDurationEvent: DurationEvent,
    useJavaDaemon = false,
  ): Promise<void> {
    const projectModel: ProjectModel = this._taskService.getProjectModel();
    const jsonProfile = this._taskService.isFaMode() ? CommonConst.CONFIG_JSON : CommonConst.APP_CONFIG;
    if (signModel?.getSignType() === SignTypeEnum.HAP || signModel?.getSignType() === SignTypeEnum.HOS_HAP) {
      const bundleNameFromP7b = await ValidateUtil.getBundleNameFromP7b(
        this.sdkInfo,
        this._signingConfig,
        pathInfo as ModulePathInfoIml,
        subDurationEvent,
        useJavaDaemon,
      );
      const bundleNameFromHap = ValidateUtil.getBundleNameFromHap(this._targetProduct, projectModel);
      if (bundleNameFromHap !== bundleNameFromP7b) {
        this._log
          .printErrorExit(
            'BUNDLE_NAME_DOES_NOT_MATCH',
            [this._taskService.getProjectModel().getProfilePath()],
            [[], [jsonProfile]]
          );
      }
    }
  }

  /**
   * 执行签名命令
   *
   * @param {SignModel} signModel
   * @param durationEvent 持续事件
   * @private
   */
  private async executeSign(signModel: SignModel, durationEvent: DurationEvent): Promise<void> {
    const localSigningConfig = this._signingConfig;
    if (!localSigningConfig) {
      // 签名配置不存在，则不执行签名
      return;
    }

    const subTaskName = `generate ${signModel.getSignType()} signing command`;
    const subDurationEvent = durationEvent.createSubEvent(subTaskName, '');
    subDurationEvent.start();

    const commandBuilder: CommonSignCommandBuilder = this._signCommandFactory.createCommandBuilder(
      this._taskService.getProjectModel(),
      localSigningConfig,
      this.sdkInfo,
      signModel,
    );
    const signCommand = commandBuilder.getSignCommand()!;

    const executeTaskName = `execute ${signModel.getSignType()} signing command`;
    if (isEqual(localSigningConfig, SignUtil.getDefaultSign())) {
      subDurationEvent.stop();
      subDurationEvent.setLog(subTaskName, MetricLogType.INFO);
      const subToolDurationEvent = durationEvent.createSubEvent(executeTaskName, '');
      subToolDurationEvent.start();

      await new ProcessUtils(this._taskService.getNode().getName(), `Sign${signModel.getSignType()}`).execute(signCommand);

      subToolDurationEvent.stop();
      subToolDurationEvent.setLog(executeTaskName, MetricLogType.INFO);
      return;
    }

    // 签名配置文件与hap包BundleName校验
    await this.validateBundleName(signModel, this._pathInfo, subDurationEvent);

    subDurationEvent.stop();
    subDurationEvent.setLog(subTaskName, MetricLogType.INFO);
    const subDurationEventWithTool = durationEvent.createSubEvent(executeTaskName, '');
    subDurationEventWithTool.start();

    if (this._taskService instanceof ModuleTaskService) {
      await new ProcessUtils(this._taskService.getModuleModel().getName(), `Sign${signModel.getSignType()}`).execute(signCommand);
    } else {
      await new ProcessUtils(this._taskService.getProjectModel().getName(), `Sign${signModel.getSignType()}`).execute(signCommand);
    }

    subDurationEventWithTool.stop();
    subDurationEventWithTool.setLog(executeTaskName, MetricLogType.INFO);
  }

  /**
   * 通过发送websocket请求执行签名命令
   *
   * @param {SignModel} signModel
   * @param durationEvent 持续事件
   * @private
   */
  private async executeSignRequest(signModel: SignModel, durationEvent: DurationEvent): Promise<any> {
    const signingConfig = this._signingConfig;
    if (!signingConfig) {
      // 签名配置不存在，则不执行签名
      return;
    }

    const subTaskEventName = `generate ${signModel.getSignType()} signing command`;
    const subDurationEvent = durationEvent.createSubEvent(subTaskEventName, '');
    subDurationEvent.start();

    const cmdBuilder: CommonSignCommandBuilder = this._signCommandFactory.createCommandBuilder(
      this._taskService.getProjectModel(),
      signingConfig,
      this.sdkInfo,
      signModel,
    );
    const signCommand = cmdBuilder.getSignCommand()!;

    const executeTaskName = `execute ${signModel.getSignType()} signing command`;

    const startSign = async () => {
      if (signModel.getSignType() === SignTypeEnum.HAR) {
        const singParams = constructHarSignParams(
          signingConfig,
          signModel.getInputFilePath(),
          signModel.getOutPutFilePath(),
          cmdBuilder.readKeyStorePwd(),
          cmdBuilder.readKeyPwd(),
        );
        await sendSignHarRequest(cmdBuilder.getSignTool(), singParams);
      } else {
        const inForm = signModel.getSignType() === SignTypeEnum.BIN ? 'bin' : 'zip';
        const singParams = constructSignParams(
          signingConfig,
          signModel.getInputFilePath(),
          signModel.getOutPutFilePath(),
          cmdBuilder.readKeyStorePwd(),
          cmdBuilder.readKeyPwd(),
          inForm,
        );
        await sendSignRequest(cmdBuilder.getSignTool(), singParams);
      }
    };

    if (isEqual(signingConfig, SignUtil.getDefaultSign())) {
      subDurationEvent.stop();
      subDurationEvent.setLog(subTaskEventName, MetricLogType.INFO);
      const subToolDurationEvent = durationEvent.createSubEvent(executeTaskName, '');
      subToolDurationEvent.start();
      await startSign();
      subToolDurationEvent.stop();
      subToolDurationEvent.setLog(executeTaskName, MetricLogType.INFO);
      return;
    }

    // 签名配置文件与hap包BundleName校验
    await this.validateBundleName(signModel, this._pathInfo, subDurationEvent, true);

    subDurationEvent.stop();
    subDurationEvent.setLog(subTaskEventName, MetricLogType.INFO);
    const subDurationEventWithTool = durationEvent.createSubEvent(executeTaskName, '');
    subDurationEventWithTool.start();
    await startSign();
    subDurationEventWithTool.stop();
    subDurationEventWithTool.setLog(executeTaskName, MetricLogType.INFO);
  }

  /**
   * 获取签名配置项
   *
   * @return {SigningConfigBuildOpt | undefined} 签名配置
   * @private
   */
  private getSigningConfig(): SigningConfigBuildOpt | undefined {
    const app = this._taskService.getProjectModel().getProfileOpt().app;
    const productSignConfigName = this._targetProduct.signingConfig;
    const signingConfigFromHvigorfile: SigningConfigBuildOpt | undefined = cloneDeep(
      projectOhosConfigManager.getOverrides()?.signingConfig as SigningConfigBuildOpt,
    );

    if (productSignConfigName === undefined) {
      if (signingConfigFromHvigorfile) {
        return signingConfigFromHvigorfile;
      }
      this.signingConfigCheckLogStr = `No signingConfig found for product ${this._targetProduct.name}`;
      return undefined;
    }
    const signingConfig: SigningConfigBuildOpt | undefined = getElementFromArr(
      app?.signingConfigs,
      productSignConfigName.substring(productSignConfigName.lastIndexOf('.') + 1),
    );

    if (signingConfig === undefined) {
      if (signingConfigFromHvigorfile) {
        return signingConfigFromHvigorfile;
      }
      this.signingConfigCheckLogStr =
        `Will skip sign '${this._signType}'. ` +
        `No signingConfigs profile is configured in current project.
             If needed, configure the signingConfigs in ${this._taskService.getProjectModel().getProfilePath()}.`;
      return undefined;
    }
    return mergeSigningConfig(signingConfig, signingConfigFromHvigorfile);
  }

  /**
   * 获取默认签名配置
   *
   * @returns {ProjectBuildProfile.SigningConfigBuildOpt}
   */
  public static getDefaultSign(): SigningConfigBuildOpt {
    return {
      material: {
        storeFile: KeyStoreHelper.getDefaultDebugKeyStoreLocation()!,
        storePassword: KeyStoreHelper.DEFAULT_STORE_PASS,
        keyAlias: KeyStoreHelper.DEFAULT_ALIAS,
        keyPassword: KeyStoreHelper.DEFAULT_STORE_PASS,
        signAlg: '', // 冗余配置 默认置空
        profile: '', // 冗余配置 默认置空
        certpath: '', // 冗余配置 默认置空
      },
      name: '', // 冗余配置 默认置空
    };
  }

  /**
   * 判断签名材料是否是默认签名
   *
   * @param {MaterialBuildOpt} currentSignMaterial
   * @returns {boolean}
   */
  public static isUseDefaultShellSign(currentSignMaterial: MaterialBuildOpt): boolean {
    const defaultSignMaterial = SignUtil.getDefaultSign().material;
    return (
      currentSignMaterial.storePassword === defaultSignMaterial.storePassword &&
      currentSignMaterial.keyPassword === defaultSignMaterial.keyPassword &&
      currentSignMaterial.storeFile === defaultSignMaterial.storeFile &&
      currentSignMaterial.keyAlias === defaultSignMaterial.keyAlias
    );
  }

  /**
   * 获取签名材料的的增量输入
   *
   * @return {Map<string, TaskInputValue>} 签名配置增量输入对象
   */
  static getSigningConfigInputs(sdkInfo: SdkInfo, signingConfig: SigningConfigBuildOpt | undefined): Map<string, TaskInputValue> {
    const inputFileMap = new Map<string, TaskInputValue>();
    inputFileMap.set(CommonConst.ENABLE_SIGN_TASK, hvigorCore.getParameter().getProperty(CommonConst.ENABLE_SIGN_TASK));
    inputFileMap.set('sdkToolchainsComponentVersion', sdkInfo.getToolchainsComponentVersion()!);
    if (!signingConfig) {
      inputFileMap.set('existSigningConfig', false);
      return inputFileMap;
    }
    inputFileMap
      .set('existSigningConfig', true)
      .set('signingConfig_name', signingConfig.name ?? '')
      .set('signingConfig_type', signingConfig.type ?? '');

    const material: MaterialBuildOpt | undefined = signingConfig.material;
    if (!material) {
      inputFileMap.set('existMaterial', false);
      return inputFileMap;
    }
    inputFileMap
      .set('existMaterial', true)
      .set('signingConfig_signAlg', material.signAlg ?? '')
      .set('signingConfig_keyAlias', material.keyAlias ?? '')
      .set('signingConfig_keyPassword', material.keyPassword ?? '')
      .set('signingConfig_storePassword', material.storePassword ?? '');

    return inputFileMap;
  }

  /**
   * 获取签名材料的的增量输入文件
   *
   * @return {FileSet} 签名配置增量文件输入对象
   */
  getSigningConfigInputFiles(): FileSet {
    const inputFiles = new FileSet();
    const material = this._signingConfig?.material;

    // 签名配置文件
    if (material?.certpath) {
      inputFiles.addEntry(FileUtil.convertToAbsolutePath(material.certpath, this._taskService.getProjectModel().getProjectDir()), { isDirectory: false });
    }
    if (material?.profile) {
      inputFiles.addEntry(FileUtil.convertToAbsolutePath(material.profile, this._taskService.getProjectModel().getProjectDir()), { isDirectory: false });
    }
    if (material?.storeFile) {
      inputFiles.addEntry(FileUtil.convertToAbsolutePath(material.storeFile, this._taskService.getProjectModel().getProjectDir()), { isDirectory: false });
    }
    return inputFiles;
  }
}

/**
 * 判断当前工程是否需要执行签名操作
 *
 * @param projectModel
 * @param isHarModule
 */
export const isSigned = (projectModel: ProjectModel, isHarModule = false): boolean => {
  const signingConfig: SigningConfigBuildOpt | undefined = getSigningConfig(projectModel);
  if (!signingConfig) {
    return false;
  }
  return isHarModule
    ? !!hvigorCore.getParameter().getProperty(HvigorConfigConst.OHOS_SIGN_HAR)
    : !!hvigorCore.getParameter().getProperty(CommonConst.ENABLE_SIGN_TASK);
};

/**
 * 获取签名配置项
 *
 * @return {SigningConfigBuildOpt | undefined} 签名配置
 * @private
 */
export function getSigningConfig(projectModel: ProjectModel): SigningConfigBuildOpt | undefined {
  const currentProduct: ProductBuildOpt = findTargetProduct(projectModel!);
  const app = projectModel.getProfileOpt().app;
  const productSignConfigName = currentProduct.signingConfig;
  const signingConfigFromHvigorfile: SigningConfigBuildOpt | undefined = cloneDeep(
    projectOhosConfigManager.getOverrides()?.signingConfig as SigningConfigBuildOpt,
  );
  if (productSignConfigName === undefined) {
    return signingConfigFromHvigorfile;
  }
  const signingConfig: SigningConfigBuildOpt | undefined = getElementFromArr(
    app?.signingConfigs,
    productSignConfigName.substring(productSignConfigName.lastIndexOf('.') + 1),
  );

  if (signingConfig === undefined) {
    return signingConfigFromHvigorfile;
  }
  return mergeSigningConfig(signingConfig, signingConfigFromHvigorfile);
}

export function mergeSigningConfig(signingConfig: SigningConfigBuildOpt, signingConfigFromHvigorfile: SigningConfigBuildOpt | undefined) {
  return {
    material: {
      ...signingConfig.material,
      ...(signingConfigFromHvigorfile?.material ?? {}),
    },
    name: signingConfig.name ?? signingConfigFromHvigorfile?.name,
    type: signingConfig.type ?? signingConfigFromHvigorfile?.type,
  };
}

/**
 * 统一获取hqf名
 * @param {string} moduleName
 * @param {string} targetName
 * @param {boolean} isSigned
 * @returns {string}
 */
export function getHqfName(moduleName: string, targetName: string, isSigned = false): string {
  return `${moduleName}-${targetName}-${isSigned ? 'signed' : 'unsigned'}.hqf`;
}